﻿using Bettzueche.RLLibrary.Functions;
using Bettzueche.RLLibrary.Types;
using System;
using System.Linq;

namespace RLProject1.Model.AMEEWorld
{
    /// <summary>
    /// Q-Funktion mit Funktionasapproximation nach LinearGradient-Abstiegsmethode und binären Features. Q(s,a)= SUM(F*theta),<para>
    /// Dabei sind theta die Funktionsparameter und F der Merkmalsvektor.</para><para>
    /// Q-Value function (auch action-value function genannt), die einem Zustands-Aktions-Paar einen Wert zuweist.<br />
    /// Q : SxA --> Double
    /// </para><para>
    /// Die Dimension des theta-Vektors ergibt sich aus Anzahl Zustände (Partitionen) * Anzahl (diskretisierter) Aktionen.
    /// </para>
    /// <para>
    /// Der Q-Wert eines Zustands s ist die zu erwartende, aufsummierte Belohnung, die ausgehend von s unter Auswahl 
    /// einer bestimmten Aktion a und der anschließenden Verfolgung einer Strategie π erreicht werden kann. Der Q-Wert
    /// ist ein Gütemaß einer Aktion in einem Zustand und seine Funktion wird üblicherweise geschätzt bzw. erlernt.
    /// </para>
    /// </summary>
    public class QFunktion : IStateValueFunction<WorldImage,AMEEAction>
    {
        /*
         * ACHTUNG! ADD-METHODEN NICHT IMPLEMENTIERT!
         */
        #region private Felder
        
        /// <summary>
        /// Parameter diese Q-Funktionsapproximation. (Q(s,a)= SUM(F*theta))
        /// </summary>
        double[] theta;

        readonly Tiling weltConfig;


        #endregion


        #region Konstruktor(en)

        /// <summary>
        /// Initializes a new instance of the <see cref="QFunktion"/> class.
        /// </summary>
        /// <param name="dimension">Dimension des 'Theta'-Vektors: Parameter der Q-Funktionsapproximation.</param>
        /// <param name="weltConfig">Referenz auf Konfigurationsobjekt der Agentenwelt. Wird readonly verwendet!</param>
        /// <exception cref="System.ArgumentException">dimension: Die Dimension des theta-Vektors muss größer 0 sein.<para>
        /// dimension mod weltConfig.Aktionen != 0 - Inkonsistenz der Daten: die Vektorgröße Theta muss der Anzahl Zustandspartitionen * Aktionspartitionen entsprechen.</para></exception>
        /// <exception cref="System.ArgumentNullException">weltConfig</exception>
        /// <seealso cref="RLProject1.Model.AMEEWorld.WorldImage"/>
        /// <remarks>die Vektorgröße Theta entspricht der Anzahl Zustandspartitionen * Aktionspartitionen</remarks>
        public QFunktion(Tiling weltConfig) : this(weltConfig.Aktionen * weltConfig.Dimension, 0.0, weltConfig) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="QFunktion"/> class.
        /// </summary>
        /// <param name="dimension">Dimension des 'Theta'-Vektors: Parameter der Q-Funktionsapproximation.</param>
        /// <param name="startwert">Initialer Wert für alle Q-Funktionsparameter (-werte).</param>
        /// <param name="weltConfig">Referenz auf Konfigurationsobjekt der Agentenwelt. Wird readonly verwendet!</param>
        /// <exception cref="System.ArgumentException">dimension: Die Dimension des theta-Vektors muss größer 0 sein.<para>
        /// dimension mod weltConfig.Aktionen != 0 - Inkonsistenz der Daten: die Vektorgröße Theta muss der Anzahl Zustandspartitionen * Aktionspartitionen entsprechen.</para></exception>
        /// <exception cref="System.ArgumentNullException">weltConfig</exception>
        /// <seealso cref="RLProject1.Model.AMEEWorld.WorldImage"/>
        /// <remarks>die Vektorgröße Theta entspricht der Anzahl Zustandspartitionen * Aktionspartitionen</remarks>
        internal QFunktion(int dimension, double startwert, Tiling weltConfig) {
            if (dimension <= 0)
                throw new ArgumentException("Die Dimension des theta-Vektors muss größer 0 sein.", "dimension");
            if (weltConfig == null)
                throw new ArgumentNullException("weltConfig");
            if (dimension % weltConfig.Aktionen != 0)
                throw new ArgumentException("dimension mod weltConfig.Aktionen != 0 - Inkonsistenz der Daten: die Vektorgröße Theta muss der Anzahl Zustandspartitionen * Aktionspartitionen entsprechen.");

            theta = Enumerable.Repeat<double>(startwert, dimension).ToArray();
            this.weltConfig = weltConfig;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="QFunktion"/> class.
        /// </summary>
        /// <param name="thetaStart">Initialer Theta-Vektor: Startparameter der Q-Funktionsapproximation.</param>
        /// <param name="weltConfig">Referenz auf Konfigurationsobjekt der Agentenwelt. Wird readonly verwendet!</param>
        /// <exception cref="System.ArgumentNullException">
        /// thetaStart
        /// or
        /// weltConfig
        /// </exception>
        /// <remarks>die Vektorgröße entspricht der Anzahl Zustandspartitionen * Aktionspartitionen</remarks>
        public QFunktion(double[] thetaStart, Tiling weltConfig) {
            if (thetaStart == null)
                throw new ArgumentNullException("thetaStart");
            if (weltConfig == null)
                throw new ArgumentNullException("weltConfig");

            theta = thetaStart;
            this.weltConfig = weltConfig;
        }

        #endregion


        #region IStateValueFunction<WorldImage, AMEEAction>

        /// <summary>
        /// Gibt oder setzt den Wert (Q-Value bzw. theta-Parameter)
        /// </summary>
        /// <param name="state">Zustand</param>
        /// <param name="action">Aktion</param>
        /// <value>Q-Wert bzw. genauer: neuer thta-Parameter</value>
        /// <returns>Q(s,a)</returns>
        public double this[WorldImage state, AMEEAction action] {
            get {
                double q_sa = 0.0;
                int[] fIndizes = GibThetaIndex(state, action);
                foreach (int idx in fIndizes) {
                    q_sa += this.theta[idx];
                }
                return q_sa;
            }
            set { // Achtung theta ist NICHT gleich Q. Eigentlich gilt theta_vec += ...e_vec
                  // theta_vec += alpha * delta * e_vec  &  e_vec *= alpha*gamma
                  // Bei EINEM Tiling und bin.Feat. geht das noch wie unten und theta = Q passt.
                int[] fIndizes = GibThetaIndex(state, action);
                foreach (int idx in fIndizes) {
                    this.theta[idx] = value;
                }
            }
        }

        /// <summary>
        /// Gibt oder setzt den Theta- (Q-) Wert.
        /// </summary>
        /// <value>Der zu setzende Theta- (Q-) Wert.</value>
        /// <param name="thetaIndex">Theta Index</param>
        /// <returns>Den Theta- (Q-) Wert.</returns>
        /// <seealso cref="QFunktion.GibThetaIndex(WorldImage,AMEEAction)"/>
        /// <exception cref="System.IndexOutOfRangeException">zu setzender Index nicht vorhanden.</exception>
        public double this[int thetaIndex] {
            get { return this.theta[thetaIndex]; }
            set { theta[thetaIndex] = value; }
        }
       

        /// <summary>
        /// Gets all values of the specified state.
        /// <para>(Optional) Hier: nicht unterstützt.</para>
        /// </summary>
        /// <param name="state">The state.</param>
        /// <returns>All Function values of the specified state.</returns>
        /// <exception cref="System.NotSupportedException">(Noch) nicht unterstützt.</exception>
        public double[] GetEstimates(WorldImage state) {
            double[] qWerte = new double[weltConfig.Aktionen];
            AMEEAction a = new AMEEAction();
            for (int aktion = 0; aktion < weltConfig.Aktionen; aktion++) {
                a.Abschnittsnummer = aktion;
                // Q(s,a)= SUM(F*theta)
                int[] fIndizes = GibThetaIndex(state, a);
                foreach (int idx in fIndizes) {
                    qWerte[a] += theta[idx];
                }
            }
            return qWerte;
        }

        #endregion


        internal int[] GibThetaIndex(WorldImage state, AMEEAction action) {
            int aktion = action; //= action.Abschnittsnummer;

            // Berechnung des theta-Index - !mehrere Tilings = mehrere indizes!
            int theta_index = aktion * weltConfig.Dimension + state.GibZustandsID(); // gibZustandIndex(state);
            return new int[]{theta_index};
        }

        #region IStateValueFunction Members

        public double this[IState state, IAction action] {
            get {
                if (state is WorldImage) {
                    if (action is AMEEAction) {
                        return this[(WorldImage)state, (AMEEAction)action];
                    }
                }
                throw new NotSupportedException();
            }
            set {
                if (state is WorldImage) {
                    if (action is GridAction) {
                        this[(WorldImage)state, (AMEEAction)action] = value;
                    }
                }
                throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Gets all values of the specified state.
        /// <para>(Optional)</para>
        /// </summary>
        /// <param name="state">The state.</param>
        /// <returns>
        /// All Function values of the specified state.
        /// </returns>
        /// <exception cref="System.NotSupportedException">state Impl. nicht unterstützt</exception>
        public double[] GetEstimates(IState state) {
            if (state is WorldImage) {
                return this.GetEstimates((WorldImage)state);
            }
            throw new NotSupportedException("state Impl. nicht unterstützt");
        }

        /// <summary>
        /// Adds the specified e function.
        /// </summary>
        /// <param name="eFunction">The e function.</param>
        /// <param name="factor">The factor.</param>
        /// <exception cref="System.NotImplementedException"></exception>
        public void Add(IEligibilityFunction<AMEEAction, WorldImage> eFunction, double factor) {
            
            throw new NotImplementedException();
        }

        public void Add(IEligibilityFunction<AMEEAction, WorldImage> eFunction) {
            throw new NotImplementedException();
        }

        public void Add(Matrix m) {
            throw new NotImplementedException();
        }

        #endregion

    }
}
